/*
 * Copyright (c) 2022-2023 Shenzhen Kaihong Digital Industry Development Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http:// www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

const { XMessage } = require('../message/XMessage');
const { NapiLog } = require('./NapiLog');

function code(s) {
  return s.charCodeAt(0);
}

function isSpace(c) {
  return c === code(' ') || c === code('\t') || c === code('\r');
}

function isalpha(c) {
  if (code('a') <= c[0] && c[0] <= code('z')) {
    return true;
  }
  if (code('A') <= c[0] && c[0] <= code('Z')) {
    return true;
  }
  return false;
}

function isalnum(c) {
  return isalpha(c) || isNum(c);
}

function isNum(c) {
  return code('0') <= c[0] && c[0] <= code('9');
}

function toStr(c) {
  return String.fromCharCode(c[0]);
}

class TokenType {}
TokenType.NUMBER = 256;
TokenType.TEMPLATE = 257;
TokenType.LITERAL = 258;
TokenType.STRING = 259;
TokenType.REF_PATH = 260;
TokenType.FILE_PATH = 261;
TokenType.ROOT = 262;
TokenType.INCLUDE = 263;
TokenType.DELETE = 264;
TokenType.BOOL = 265;
TokenType.EOF = -1;

class Lexer {
  constructor() {
    this.keyWords_ = {
      '#include': TokenType.INCLUDE,
      root: TokenType.ROOT,
      delete: TokenType.DELETE,
      template: TokenType.TEMPLATE,
    };
  }

  initialize(sourceName) {
    this.srcName_ = sourceName;
    if (!(sourceName in Lexer.FILE_AND_DATA)) {
      XMessage.gi().send('getfiledata', sourceName);
      return false;
    }
    this.data_ = Lexer.FILE_AND_DATA[sourceName];
    NapiLog.logError('------------data start----------------');
    NapiLog.logError(sourceName);
    NapiLog.logError(Lexer.FILE_AND_DATA[sourceName]);
    NapiLog.logError('------------data end------------------');

    this.bufferStart_ = 0;
    this.bufferEnd_ = this.data_.length - 1;
    this.lineno_ = 1;
    this.lineLoc_ = 1;

    return true;
  }

  lexInclude(token) {
    this.consumeChar();
    this.lexFromLiteral(token);
    if (token.strval !== 'include') {
      return false;
    }

    token.type = TokenType.INCLUDE;
    return true;
  }

  isConsumeCharCode(c) {
    if (
      c[0] === code(';') ||
      c[0] === code(',') ||
      c[0] === code('[') ||
      c[0] === code(']') ||
      c[0] === code('{') ||
      c[0] === code('}') ||
      c[0] === code('=') ||
      c[0] === code('&') ||
      c[0] === code(':')
    ) {
      return true;
    }
    return false;
  }

  lex(token) {
    let c = [];
    this.initToken(token);
    do {
      if (!this.peekChar(c, true)) {
        token.type = TokenType.EOF;
        return true;
      }
      if (c[0] === code('#')) {
        return this.lexInclude(token);
      }
      if (isalpha(c)) {
        this.lexFromLiteral(token);
        return true;
      }

      if (isNum(c)) {
        return this.lexFromNumber(token);
      }

      if (c[0] === code('/')) {
        if (!this.processComment()) {
          return false;
        }
      } else if (this.isConsumeCharCode(c)) {
        this.consumeChar();
        token.type = c[0];
        token.lineNo = this.lineno_;
        break;
      } else if (c[0] === code('"')) {
        return this.lexFromString(token);
      } else if (c[0] === code('+') || c[0] === code('-')) {
        return lexFromNumber(token);
      } else if (c[0] === -1) {
        token.type = -1;
        break;
      } else {
        dealWithError(
          "can not recognized character '" + toStr(c) + "'" + this.bufferStart_
        );
        return false;
      }
    } while (true);
    return true;
  }

  peekChar(c, skipSpace) {
    if (skipSpace) {
      while (
        this.bufferStart_ <= this.bufferEnd_ &&
        (isSpace(this.data_[this.bufferStart_]) ||
          this.data_[this.bufferStart_] === code('\n'))
      ) {
        this.lineLoc_++;
        if (this.data_[this.bufferStart_] === code('\n')) {
          this.lineLoc_ = 0;
          this.lineno_++;
        }
        this.bufferStart_++;
      }
    }

    if (this.bufferStart_ > this.bufferEnd_) {
      return false;
    }
    c[0] = this.data_[this.bufferStart_];
    return true;
  }

  initToken(token) {
    token.type = 0;
    token.numval = 0;
    token.strval = '';
    token.src = this.srcName_;
    token.lineNo = this.lineno_;
  }

  lexFromLiteral(token) {
    let value = '';
    let c = [];

    while (this.peekChar(c, false) && !isSpace(c[0])) {
      if (!isalnum(c) && c[0] !== code('_') && c[0] !== code('.') && c[0] !== code('\\')) {
        break;
      }
      value += toStr(c);
      this.consumeChar();
    }

    do {
      if (value === 'true') {
        token.type = TokenType.BOOL;
        token.numval = 1;
        break;
      } else if (value === 'false') {
        token.type = TokenType.BOOL;
        token.numval = 0;
        break;
      }

      if (value in this.keyWords_) {
        token.type = this.keyWords_[value];
        break;
      }

      if (value.indexOf('.') >= 0) {
        token.type = TokenType.REF_PATH;
      } else {
        token.type = TokenType.LITERAL;
      }
    } while (false);

    token.strval = value;
    token.lineNo = this.lineno_;
  }
  getRawChar() {
    this.lineLoc_++;
    let ret = this.data_[this.bufferStart_];
    this.bufferStart_++;
    return ret;
  }
  getChar(c, skipSpace) {
    let chr = this.getRawChar();
    if (skipSpace) {
      while (isSpace(chr)) {
        chr = this.getRawChar();
      }
    }

    if (chr === code('\n')) {
      this.lineno_++;
      this.lineLoc_ = 0;
    }
    c[0] = chr;
    return true;
  }
  consumeChar() {
    let c = [];
    this.getChar(c, false);
  }

  lexFromOctalNumber(c, param) {
    while (this.peekChar(c) && isNum(c)) {
      this.consumeChar();
      param.value += toStr(c);
    }
    param.v = parseInt(param.value, 8);
    param.baseSystem = 8;
  }

  lexFromHexNumber(c, param) {
    this.consumeChar();
    while (
      this.peekChar(c, false) &&
      (isNum(c) ||
        (c[0] >= code('a') && c[0] <= code('f')) ||
        (c[0] >= code('A') && c[0] <= code('F')))
    ) {
      param.value += toStr(c);
      this.consumeChar();
    }
    param.v = parseInt(param.value, 16);
    param.baseSystem = 16;
  }

  lexFromBinaryNumber(c, param) {
    this.consumeChar();
    while (
      this.peekChar(c, false) &&
      (c[0] === code('0') || c[0] === code('1'))
    ) {
      param.value += toStr(c);
      this.consumeChar();
    }
    param.v = parseInt(param.value, 2);
    param.baseSystem = 2;
  }

  lexFromNumber(token) {
    let c = [];

    let errno = 0;
    let param = {
      value: '',
      v: 0,
      baseSystem: 10,
    };

    this.getChar(c, false);
    switch (c[0]) {
      case code('0'):
        if (!this.peekChar(c, true)) {
          break;
        }
        if (isNum(c)) {
          this.lexFromOctalNumber(c, param);
          break;
        }
        if (c[0] === code('x') || code('x') === code('X')) {
          this.lexFromHexNumber(c, param);
          break;
        } else if (c[0] === code('b')) {
          this.lexFromBinaryNumber(c, param);
          break;
        }
        break;
      case code('+'):
      case code('-'):
      default:
        param.value += toStr(c);
        while (this.peekChar(c, true) && isNum(c)) {
          this.consumeChar();
          param.value += toStr(c);
        }
        let baseSystem = 10;
        param.v = BigInt(param.value, baseSystem);
        param.baseSystem = baseSystem;
        break;
    }

    if (errno !== 0) {
      dealWithError('illegal number: ' + param.value);
      return false;
    }
    token.type = TokenType.NUMBER;
    token.numval = param.v;
    token.lineNo = this.lineno_;
    token.baseSystem = param.baseSystem;
    return true;
  }

  lexFromString(token) {
    let c = [];
    this.getChar(c, false);
    let value = '';
    while (this.getChar(c, false) && c[0] !== code('"')) {
      value += toStr(c);
    }

    if (c[0] !== code('"')) {
      dealWithError('unterminated string');
      return false;
    }
    token.type = TokenType.STRING;
    token.strval = value;
    token.lineNo = this.lineno_;
    return true;
  }
  processComment() {
    let c = [];
    this.consumeChar();
    if (!this.getChar(c, true)) {
      dealWithError('unterminated comment');
      return false;
    }

    if (c[0] === code('/')) {
      while (c[0] !== code('\n')) {
        if (!this.getChar(c, true)) {
          break;
        }
      }
      if (c[0] !== code('\n') && c[0] !== TokenType.EOF) {
        dealWithError('unterminated signal line comment');
        return false;
      }
    } else if (c[0] === code('*')) {
      while (this.getChar(c, true)) {
        if (c[0] === code('*') && this.getChar(c, true) && c[0] === code('/')) {
          return true;
        }
      }
      if (c[0] !== code('/')) {
        dealWithError('unterminated multi-line comment');
        return false;
      }
    } else {
      dealWithError('invalid character');
      return false;
    }

    return true;
  }

  dealWithError(message) {
    XMessage.gi().send('error', message);
    NapiLog.logError(message + "'");
  }
}
Lexer.FILE_AND_DATA = {};

module.exports = {
  Lexer,
  TokenType,
  code,
};
